import { FileLoader, Loader, Matrix4, Vector3 } from 'three'
import { gunzipSync } from 'fflate'
import { Volume } from '../misc/Volume'

class NRRDLoader extends Loader {
  constructor(manager) {
    super(manager)
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this

    const loader = new FileLoader(scope.manager)
    loader.setPath(scope.path)
    loader.setResponseType('arraybuffer')
    loader.setRequestHeader(scope.requestHeader)
    loader.setWithCredentials(scope.withCredentials)
    loader.load(
      url,
      function (data) {
        try {
          onLoad(scope.parse(data))
        } catch (e) {
          if (onError) {
            onError(e)
          } else {
            console.error(e)
          }

          scope.manager.itemError(url)
        }
      },
      onProgress,
      onError,
    )
  }

  parse(data) {
    // this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X

    let _data = data

    let _dataPointer = 0

    const _nativeLittleEndian = new Int8Array(new Int16Array([1]).buffer)[0] > 0

    const _littleEndian = true

    const headerObject = {}

    function scan(type, chunks) {
      if (chunks === undefined || chunks === null) {
        chunks = 1
      }

      let _chunkSize = 1
      let _array_type = Uint8Array

      switch (type) {
        // 1 byte data types
        case 'uchar':
          break
        case 'schar':
          _array_type = Int8Array
          break
        // 2 byte data types
        case 'ushort':
          _array_type = Uint16Array
          _chunkSize = 2
          break
        case 'sshort':
          _array_type = Int16Array
          _chunkSize = 2
          break
        // 4 byte data types
        case 'uint':
          _array_type = Uint32Array
          _chunkSize = 4
          break
        case 'sint':
          _array_type = Int32Array
          _chunkSize = 4
          break
        case 'float':
          _array_type = Float32Array
          _chunkSize = 4
          break
        case 'complex':
          _array_type = Float64Array
          _chunkSize = 8
          break
        case 'double':
          _array_type = Float64Array
          _chunkSize = 8
          break
      }

      // increase the data pointer in-place
      let _bytes = new _array_type(_data.slice(_dataPointer, (_dataPointer += chunks * _chunkSize)))

      // if required, flip the endianness of the bytes
      if (_nativeLittleEndian != _littleEndian) {
        // we need to flip here since the format doesn't match the native endianness
        _bytes = flipEndianness(_bytes, _chunkSize)
      }

      if (chunks == 1) {
        // if only one chunk was requested, just return one value
        return _bytes[0]
      }

      // return the byte array
      return _bytes
    }

    //Flips typed array endianness in-place. Based on https://github.com/kig/DataStream.js/blob/master/DataStream.js.

    function flipEndianness(array, chunkSize) {
      const u8 = new Uint8Array(array.buffer, array.byteOffset, array.byteLength)
      for (let i = 0; i < array.byteLength; i += chunkSize) {
        for (let j = i + chunkSize - 1, k = i; j > k; j--, k++) {
          const tmp = u8[k]
          u8[k] = u8[j]
          u8[j] = tmp
        }
      }

      return array
    }

    //parse the header
    function parseHeader(header) {
      let data, field, fn, i, l, m, _i, _len
      const lines = header.split(/\r?\n/)
      for (_i = 0, _len = lines.length; _i < _len; _i++) {
        l = lines[_i]
        if (l.match(/NRRD\d+/)) {
          headerObject.isNrrd = true
        } else if (l.match(/^#/)) {
        } else if ((m = l.match(/(.*):(.*)/))) {
          field = m[1].trim()
          data = m[2].trim()
          fn = _fieldFunctions[field]
          if (fn) {
            fn.call(headerObject, data)
          } else {
            headerObject[field] = data
          }
        }
      }

      if (!headerObject.isNrrd) {
        throw new Error('Not an NRRD file')
      }

      if (headerObject.encoding === 'bz2' || headerObject.encoding === 'bzip2') {
        throw new Error('Bzip is not supported')
      }

      if (!headerObject.vectors) {
        //if no space direction is set, let's use the identity
        headerObject.vectors = [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1)]
        //apply spacing if defined
        if (headerObject.spacings) {
          for (i = 0; i <= 2; i++) {
            if (!isNaN(headerObject.spacings[i])) {
              headerObject.vectors[i].multiplyScalar(headerObject.spacings[i])
            }
          }
        }
      }
    }

    //parse the data when registred as one of this type : 'text', 'ascii', 'txt'
    function parseDataAsText(data, start, end) {
      let number = ''
      start = start || 0
      end = end || data.length
      let value
      //length of the result is the product of the sizes
      const lengthOfTheResult = headerObject.sizes.reduce(function (previous, current) {
        return previous * current
      }, 1)

      let base = 10
      if (headerObject.encoding === 'hex') {
        base = 16
      }

      const result = new headerObject.__array(lengthOfTheResult)
      let resultIndex = 0
      let parsingFunction = parseInt
      if (headerObject.__array === Float32Array || headerObject.__array === Float64Array) {
        parsingFunction = parseFloat
      }

      for (let i = start; i < end; i++) {
        value = data[i]
        //if value is not a space
        if ((value < 9 || value > 13) && value !== 32) {
          number += String.fromCharCode(value)
        } else {
          if (number !== '') {
            result[resultIndex] = parsingFunction(number, base)
            resultIndex++
          }

          number = ''
        }
      }

      if (number !== '') {
        result[resultIndex] = parsingFunction(number, base)
        resultIndex++
      }

      return result
    }

    const _bytes = scan('uchar', data.byteLength)
    const _length = _bytes.length
    let _header = null
    let _data_start = 0
    let i
    for (i = 1; i < _length; i++) {
      if (_bytes[i - 1] == 10 && _bytes[i] == 10) {
        // we found two line breaks in a row
        // now we know what the header is
        _header = this.parseChars(_bytes, 0, i - 2)
        // this is were the data starts
        _data_start = i + 1
        break
      }
    }

    // parse the header
    parseHeader(_header)

    _data = _bytes.subarray(_data_start) // the data without header
    if (headerObject.encoding.substring(0, 2) === 'gz') {
      // we need to decompress the datastream
      // here we start the unzipping and get a typed Uint8Array back
      _data = gunzipSync(new Uint8Array(_data))
    } else if (
      headerObject.encoding === 'ascii' ||
      headerObject.encoding === 'text' ||
      headerObject.encoding === 'txt' ||
      headerObject.encoding === 'hex'
    ) {
      _data = parseDataAsText(_data)
    } else if (headerObject.encoding === 'raw') {
      //we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header
      const _copy = new Uint8Array(_data.length)

      for (let i = 0; i < _data.length; i++) {
        _copy[i] = _data[i]
      }

      _data = _copy
    }

    // .. let's use the underlying array buffer
    _data = _data.buffer

    const volume = new Volume()
    volume.header = headerObject
    //
    // parse the (unzipped) data to a datastream of the correct type
    //
    volume.data = new headerObject.__array(_data)
    // get the min and max intensities
    const min_max = volume.computeMinMax()
    const min = min_max[0]
    const max = min_max[1]
    // attach the scalar range to the volume
    volume.windowLow = min
    volume.windowHigh = max

    // get the image dimensions
    volume.dimensions = [headerObject.sizes[0], headerObject.sizes[1], headerObject.sizes[2]]
    volume.xLength = volume.dimensions[0]
    volume.yLength = volume.dimensions[1]
    volume.zLength = volume.dimensions[2]
    // spacing
    const spacingX = new Vector3(
      headerObject.vectors[0][0],
      headerObject.vectors[0][1],
      headerObject.vectors[0][2],
    ).length()
    const spacingY = new Vector3(
      headerObject.vectors[1][0],
      headerObject.vectors[1][1],
      headerObject.vectors[1][2],
    ).length()
    const spacingZ = new Vector3(
      headerObject.vectors[2][0],
      headerObject.vectors[2][1],
      headerObject.vectors[2][2],
    ).length()
    volume.spacing = [spacingX, spacingY, spacingZ]

    // Create IJKtoRAS matrix
    volume.matrix = new Matrix4()

    let _spaceX = 1
    let _spaceY = 1
    const _spaceZ = 1

    if (headerObject.space == 'left-posterior-superior') {
      _spaceX = -1
      _spaceY = -1
    } else if (headerObject.space === 'left-anterior-superior') {
      _spaceX = -1
    }

    if (!headerObject.vectors) {
      volume.matrix.set(_spaceX, 0, 0, 0, 0, _spaceY, 0, 0, 0, 0, _spaceZ, 0, 0, 0, 0, 1)
    } else {
      const v = headerObject.vectors

      volume.matrix.set(
        _spaceX * v[0][0],
        _spaceX * v[1][0],
        _spaceX * v[2][0],
        0,
        _spaceY * v[0][1],
        _spaceY * v[1][1],
        _spaceY * v[2][1],
        0,
        _spaceZ * v[0][2],
        _spaceZ * v[1][2],
        _spaceZ * v[2][2],
        0,
        0,
        0,
        0,
        1,
      )
    }

    volume.inverseMatrix = new Matrix4()
    volume.inverseMatrix.copy(volume.matrix).invert()
    volume.RASDimensions = new Vector3(volume.xLength, volume.yLength, volume.zLength)
      .applyMatrix4(volume.matrix)
      .round()
      .toArray()
      .map(Math.abs)

    // .. and set the default threshold
    // only if the threshold was not already set
    if (volume.lowerThreshold === -Infinity) {
      volume.lowerThreshold = min
    }

    if (volume.upperThreshold === Infinity) {
      volume.upperThreshold = max
    }

    return volume
  }

  parseChars(array, start, end) {
    // without borders, use the whole array
    if (start === undefined) {
      start = 0
    }

    if (end === undefined) {
      end = array.length
    }

    let output = ''
    // create and append the chars
    let i = 0
    for (i = start; i < end; ++i) {
      output += String.fromCharCode(array[i])
    }

    return output
  }
}

const _fieldFunctions = {
  type: function (data) {
    switch (data) {
      case 'uchar':
      case 'unsigned char':
      case 'uint8':
      case 'uint8_t':
        this.__array = Uint8Array
        break
      case 'signed char':
      case 'int8':
      case 'int8_t':
        this.__array = Int8Array
        break
      case 'short':
      case 'short int':
      case 'signed short':
      case 'signed short int':
      case 'int16':
      case 'int16_t':
        this.__array = Int16Array
        break
      case 'ushort':
      case 'unsigned short':
      case 'unsigned short int':
      case 'uint16':
      case 'uint16_t':
        this.__array = Uint16Array
        break
      case 'int':
      case 'signed int':
      case 'int32':
      case 'int32_t':
        this.__array = Int32Array
        break
      case 'uint':
      case 'unsigned int':
      case 'uint32':
      case 'uint32_t':
        this.__array = Uint32Array
        break
      case 'float':
        this.__array = Float32Array
        break
      case 'double':
        this.__array = Float64Array
        break
      default:
        throw new Error('Unsupported NRRD data type: ' + data)
    }

    return (this.type = data)
  },

  endian: function (data) {
    return (this.endian = data)
  },

  encoding: function (data) {
    return (this.encoding = data)
  },

  dimension: function (data) {
    return (this.dim = parseInt(data, 10))
  },

  sizes: function (data) {
    let i
    return (this.sizes = (function () {
      const _ref = data.split(/\s+/)
      const _results = []

      for (let _i = 0, _len = _ref.length; _i < _len; _i++) {
        i = _ref[_i]
        _results.push(parseInt(i, 10))
      }

      return _results
    })())
  },

  space: function (data) {
    return (this.space = data)
  },

  'space origin': function (data) {
    return (this.space_origin = data.split('(')[1].split(')')[0].split(','))
  },

  'space directions': function (data) {
    let f, v
    const parts = data.match(/\(.*?\)/g)
    return (this.vectors = (function () {
      const _results = []

      for (let _i = 0, _len = parts.length; _i < _len; _i++) {
        v = parts[_i]
        _results.push(
          (function () {
            const _ref = v.slice(1, -1).split(/,/)
            const _results2 = []

            for (let _j = 0, _len2 = _ref.length; _j < _len2; _j++) {
              f = _ref[_j]
              _results2.push(parseFloat(f))
            }

            return _results2
          })(),
        )
      }

      return _results
    })())
  },

  spacings: function (data) {
    let f
    const parts = data.split(/\s+/)
    return (this.spacings = (function () {
      const _results = []

      for (let _i = 0, _len = parts.length; _i < _len; _i++) {
        f = parts[_i]
        _results.push(parseFloat(f))
      }

      return _results
    })())
  },
}

export { NRRDLoader }
